package com.snlab;

import jdd.bdd.BDD;

import java.util.*;

public class Device {
    private String name;
    private HashSet<Rule> rules;
    HashMap<String, Port> ports = new HashMap<>();

    private HashSet<Integer> transferredPred;

    public Device(String name) {
        this.name = name;
        this.rules = new HashSet<>();

        // add default rules to fib
        Port defaultPort = new Port(name + "-default", this);
        this.ports.put(name + "-default", defaultPort);
        defaultPort.getPredicates().add(1);
        Rule rule = new Rule(0, 0, defaultPort);
        this.addRule(rule);
        Verifier.predToPorts.get(1).add(defaultPort);
        System.out.println(rule.getHit());
    }

    public Port addPortByName(String name) {
        Port p = new Port(name, this);
        this.ports.put(name, p);
        return p;
    }

    public Port getPortByName(String name) {
        return this.ports.get(name);
    }

    public void addRule(Rule rule) {
        this.transferredPred = new HashSet<>();
        HashSet<Change> changes = this.identifyChangesInsert(rule);
        this.update(changes);
    }

    private HashSet<Change> identifyChangesInsert(Rule rule) {
        HashSet<Change> changes = new HashSet<>();
        BDD bdd = BDDEngine.getInstance().bdd;
        // entrybdd already referred
        int entrybdd = BDDEngine.getInstance().encodeDstIPPrefix(rule.getDstIp(), rule.getPrefix());
        rule.setHit(entrybdd);
        boolean bddused = false;
        for (Rule r : rules) {
            int tmp = bdd.and(r.getHit(), rule.getHit());
            bdd.ref(tmp);
            if (r.getPrefix() > rule.getPrefix() && tmp != BDDEngine.BDDFalse) {
                int tmp1 = bdd.ref(bdd.not(r.getHit()));
                int tmp2 = bdd.ref(bdd.and(rule.getHit(), tmp1));
                bdd.deref(tmp1);
                bdd.deref(rule.getHit());
                rule.setHit(tmp2);
            }

            if (r.getPrefix() < rule.getPrefix() && tmp != BDDEngine.BDDFalse) {
                if (r.getPort() != rule.getPort()) {
                    changes.add(new Change(tmp, r.getPort(), rule.getPort()));
                    // this bdd is used for handling changes, should not deref
                    bddused = true;
                }

                int tmp1 = bdd.ref(bdd.not(rule.getHit()));
                int tmp2 = bdd.ref(bdd.and(r.getHit(), tmp1));
                bdd.deref(tmp1);
                bdd.deref(r.getHit());
                r.setHit(tmp2);
            }
            if (!bddused) {
                bdd.deref(tmp);
            }
            bddused = false;
        }
        this.rules.add(rule);
        return changes;
    }

    private void update1(HashSet<Change> changes) {
        System.out.println(changes.size());
        BDD bdd = BDDEngine.getInstance().bdd;
        for (Change c : changes) {
            int delta = c.getHit();
//            for (Iterator<Integer> iterator = c.getOldPort().getPredicates().iterator(); iterator.hasNext();) {
//                int p = iterator.next();
            for (int p : (HashSet<Integer>)c.getOldPort().getPredicates().clone()) {
                int tmp = bdd.ref(bdd.and(p, delta));
                if (tmp != BDDEngine.BDDFalse) {
                    if (tmp != p) {
                        int tmp2 = bdd.ref(bdd.not(c.getHit()));
                        int tmp3 = bdd.ref(bdd.and(p, tmp2));
                        bdd.deref(tmp2);
//                        this.split(p, tmp, tmp3);
                    }
                    int tmp5 = bdd.ref(bdd.not(p));
                    int newdelta = bdd.ref(bdd.and(delta, tmp5));
                    bdd.deref(delta);
                    delta = newdelta;
                    bdd.deref(tmp5);
                }
            }
        }
    }

    private void update(HashSet<Change> changes) {
        BDD bdd = BDDEngine.getInstance().bdd;
        for (Change c : changes) {
            int delta = c.getHit();
            HashSet<Integer> oldPortPreds = (HashSet<Integer>) c.getOldPort().getPredicates().clone();
            for (int p : oldPortPreds) {
                int tmp = bdd.ref(bdd.and(p, delta));
                if (tmp != BDDEngine.BDDFalse) {
                    // Warning: need to check whether bdd can compare the intersection
                    // so tmp != p should not work even tmp and p has same structure
                    // since they are different pointers
//                    int tmp1 = bdd.ref(bdd.not(delta));
//                    int tmp11 = bdd.ref(bdd.and(p, tmp1));
//                    bdd.deref(tmp1);
                    if (tmp != p) {
//                    if (tmp11 != BDDEngine.BDDFalse) {
                        int tmp2 = bdd.ref(bdd.not(c.getHit()));
                        int tmp3 = bdd.ref(bdd.and(p, tmp2));
                        bdd.deref(tmp2);
                        this.split(p, tmp, tmp3);
//                        bdd.deref(tmp3);
                    }
//                    bdd.deref(tmp11);
                    this.transfer(tmp, c.getOldPort(), c.getNewPort());
                    if (true) { // do merge or not
                        HashMap<Integer, HashSet<Port>> predToPorts = (HashMap<Integer, HashSet<Port>>) Verifier.predToPorts.clone();
//                        for (int pp : c.getNewPort().getPredicates()) {
//                            if (predToPorts.get(pp).equals())
//                        }
                        HashSet<Integer> oldPreds = (HashSet<Integer>) c.getNewPort().getPredicates().clone();
                        for (int pp : oldPreds) {
                            if (pp != tmp && predToPorts.get(pp).equals(predToPorts.get(tmp))) {
                                int tmp4 = bdd.ref(bdd.or(tmp, pp));
                                this.merge(tmp, pp, tmp4);
                            }
//                            for (int ppp : predToPorts.keySet()) {
//                                if (pp != ppp) {
//                                    if (predToPorts.get(ppp).equals(predToPorts.get(pp))) {
//                                        int tmp4 = bdd.ref(bdd.or(ppp, pp));
//                                        this.merge(ppp, pp, tmp4);
////                                        bdd.deref(pp);
////                                        bdd.deref(ppp);
//                                    }
//                                }
//                            }
                        }
                    }
                    int tmp5 = bdd.ref(bdd.not(p));
                    int newdelta = bdd.ref(bdd.and(delta, tmp5));
                    bdd.deref(delta);
                    delta = newdelta;
                    bdd.deref(tmp5);
                }
//                bdd.deref(tmp);
            }
            bdd.deref(delta);
        }
    }

    private void split(int p, int p1, int p2) {
        for (Port port : Verifier.predToPorts.get(p)) {
            port.getPredicates().add(p1);
            port.getPredicates().add(p2);
            port.getPredicates().remove(p);
        }
        Verifier.predToPorts.put(p1, (HashSet<Port>) Verifier.predToPorts.get(p).clone());
        Verifier.predToPorts.put(p2, (HashSet<Port>) Verifier.predToPorts.get(p).clone());
        Verifier.predToPorts.remove(p);
        // here we derefer p
        BDDEngine.getInstance().bdd.deref(p);

        if (this.transferredPred.contains(p)) {
            this.transferredPred.add(p1);
            this.transferredPred.add(p2);
            this.transferredPred.remove(p);
        }
    }

    private void transfer(int p, Port from, Port to) {
        from.getPredicates().remove(p);
        to.getPredicates().add(p);
//        Verifier.getPredToPorts().put()
//        System.out.println((p));
        Verifier.predToPorts.get(p).add(to);
        Verifier.predToPorts.get(p).remove(from);
        this.transferredPred.add(p);
    }

    private void merge(int p1, int p2, int p) {
        for (Port port : Verifier.predToPorts.get(p1)) {
            port.getPredicates().add(p);
            port.getPredicates().removeAll(Arrays.asList(p1, p2));
        }
//        Verifier.getPredToPorts().get(p).clear();
        Verifier.predToPorts.put(p, (HashSet<Port>) Verifier.predToPorts.get(p1).clone());
        Verifier.predToPorts.remove(p1);
        Verifier.predToPorts.remove(p2);
//        Verifier.getPredToPorts().get(p).addAll(Verifier.getPredToPorts().get(p1));
        if (this.transferredPred.contains(p1) || this.transferredPred.contains(p2)) {
            this.transferredPred.add(p);
            this.transferredPred.removeAll(Arrays.asList(p1, p2));
        }
    }

    public static void main(String[] args) {
        BDD bdd = new BDD(1000, 100);
        int v1 = bdd.createVar();
        int v2 = bdd.createVar();
        System.out.println(bdd.or(v1, v2));
        System.out.println(bdd.or(v1, bdd.or(bdd.and(v1, bdd.not(v1)), v2)));
        int a = 0;
        long start = System.nanoTime();
        for (int i = 0; i < 2000; i++) {

        }
        long end = System.nanoTime();
        System.out.println(end-start);
    }
}
